package IC.Context;

import IC.AST.*;
import IC.AST.PrimitiveType;
import IC.Types.*;

/**
 * Created by IntelliJ IDEA.
 * User: nimrod
 * Date: 09/01/12
 * Time: 23:37
 * To change this template use File | Settings | File Templates.
 */

/*
    This visitor traverses the AST nodes and constructs the type table. 
	It resolves the types of various AST nodes.  
	It also checks for illegal 'extends' usage (for undefined classes 
	or classes defined later in the program) and usage of undefined classes anywhere in the program.
 */
public class BuildTypeTableVisitor implements Visitor {
    private ContextError error;
    private TypeTable table;

    public BuildTypeTableVisitor(String fileName) {
        this.error = null;
        this.table = new TypeTable(fileName);
    }

    public ContextError getError() {
        return error;
    }

    public TypeTable getTable() {
        return table;
    }

    @Override
    public Object visit(Program program) {
        for (ICClass cls : program.getClasses()) {
            ClassType type = this.table.getClassType(cls);
            if (type == null) {
                if (this.table.getClassTypeByName(cls.getName()) != null) {
                    this.error = new ContextError("Class '"
                            + cls.getSuperClassName() +"' is defined more than once.",
                            cls.getLine());
                } else {
                    this.error = new ContextError("Class extends type '"
                            + cls.getSuperClassName() +"' that wasn't previously defined.",
                            cls.getLine());
                }
                return null;
            }
        }
        for (ICClass cls : program.getClasses()) {
            cls.accept(this);
        }
        return null;
    }

    @Override
    public Object visit(ICClass icClass) {
        for (Method method : icClass.getMethods()) {
            MethodType mt =  (MethodType)method.accept(this);
            if (mt == null) return null;
        }

        for (Field field : icClass.getFields()) {
            IC.Types.Type ft = (IC.Types.Type)field.accept(this);
            if (ft == null) return null;
        }

        return null;
    }

    private MethodType visitMethod(Method method) {
        IC.Types.Type[] paramTypes = new IC.Types.Type[method.getFormals().size()];
        for (int i = 0; i < paramTypes.length; i++) {
            paramTypes[i] = (IC.Types.Type)method.getFormals().get(i).accept(this);
            if (paramTypes[i] == null) return null;
        }

        IC.Types.Type returnType = (IC.Types.Type)method.getType().accept(this);
        if (returnType == null) return null;

        for (Statement s : method.getStatements()) {
            s.accept(this);
            if (this.error != null) return null;
        }

        MethodType mt = this.table.getMethodType(paramTypes, returnType);
        method.setContextType(mt);
        return mt;
    }

    @Override
    public Object visit(Field field) {
        return field.getType().accept(this);
    }

    @Override
    public Object visit(VirtualMethod method) {
        return this.visitMethod(method);
    }

    @Override
    public Object visit(StaticMethod method) {
        return this.visitMethod(method);
    }

    @Override
    public Object visit(LibraryMethod method) {
        return this.visitMethod(method);
    }

    @Override
    public Object visit(Formal formal) {
        return formal.getType().accept(this);
    }

    @Override
    public Object visit(PrimitiveType type) {
        IC.Types.Type res = null;
        switch (type.getType()) {
            case BOOLEAN:
                res = this.table.getBoolType();
                break;
            case INT:
                res = this.table.getIntType();
                break;
            case STRING:
                res = this.table.getStringType();
                break;
            case VOID:
                res = this.table.getVoidType();
                break;
        }

        res = this.table.getArrayType(res, type.getDimension());
        type.setContextType(res);

        return res;
    }

    @Override
    public Object visit(UserType type) {
        IC.Types.Type userType = this.table.getClassTypeByName(type.getName());
        if (userType == null) {
            this.error = new ContextError("Cannot resolve type: '" + type.getName() + "'", type.getLine());
            return null;
        }
        userType = this.table.getArrayType(userType, type.getDimension());
        type.setContextType(userType);
        return  userType;
    }

    @Override
    public Object visit(Assignment assignment) {
        assignment.getVariable().accept(this);
        if (this.error != null) return null;
        assignment.getAssignment().accept(this);
        return null;
    }

    @Override
    public Object visit(CallStatement callStatement) {
        return callStatement.getCall().accept(this);
    }

    @Override
    public Object visit(Return returnStatement) {
        if (returnStatement.hasValue()) {
            return returnStatement.getValue().accept(this);
        }
        return null;
    }

    @Override
    public Object visit(If ifStatement) {
        ifStatement.getCondition().accept(this);

        if (this.error != null) return null;

        ifStatement.getOperation().accept(this);

        if (this.error != null) return null;
        
        if (ifStatement.hasElse())
            ifStatement.getElseOperation().accept(this);

        return null;
    }

    @Override
    public Object visit(While whileStatement) {

        whileStatement.getCondition().accept(this);

        if (this.error != null) return null;

        whileStatement.getOperation().accept(this);

        return null;
    }

    @Override
    public Object visit(Break breakStatement) {
        return null;
    }

    @Override
    public Object visit(Continue continueStatement) {
        return null;
    }

    @Override
    public Object visit(StatementsBlock statementsBlock) {
        for (Statement s : statementsBlock.getStatements()) {
            s.accept(this);
            if (this.error != null) return null;
        }
        return null;
    }

    @Override
    public Object visit(LocalVariable localVariable) {
        localVariable.getType().accept(this);

        if (this.error != null) return null;

        if (localVariable.hasInitValue()) {
            localVariable.getInitValue().accept(this);
        }

        return null;
    }

    @Override
    public Object visit(VariableLocation location) {
        if (location.isExternal()) {
            location.getLocation().accept(this);
        }
        return null;
    }

    @Override
    public Object visit(ArrayLocation location) {
        location.getArray().accept(this);
        if (this.error != null) return null;
        location.getIndex().accept(this);
        return null;
    }
    
    private void visitCall(Call call) {
        for (Expression arg : call.getArguments()) {
            arg.accept(this);
            if (this.error != null) return;
        }
    }

    @Override
    public Object visit(StaticCall call) {
        visitCall(call);
        if (this.error != null) return null;

        ClassType classType = this.table.getClassTypeByName(call.getClassName());
        if (classType == null) {
            this.error = new ContextError("Static call uses an unknown type '" + call.getClassName() + "'",
                                                call.getLine());
        }

        call.setClassContextType(classType);

        return null;
    }

    @Override
    public Object visit(VirtualCall call) {
        visitCall(call);
        if (this.error != null) return null;

        if (call.isExternal()) {
            call.getLocation().accept(this);
        }

        return null;
    }

    @Override
    public Object visit(This thisExpression) {
        return null;
    }

    @Override
    public Object visit(NewClass newClass) {
        IC.Types.Type classType = this.table.getClassTypeByName(newClass.getName());
        if (classType == null) {
            this.error = new ContextError("Unknown type instantiation '" + newClass.getName() + "'",
                                                newClass.getLine());
        }
        newClass.setContextType(classType);
        return null;
    }

    @Override
    public Object visit(NewArray newArray) {
        newArray.getType().accept(this);
        if (this.error != null) return null;
        newArray.getSize().accept(this);
		
        IC.Types.Type elementType = newArray.getType().getContextType();
        newArray.setContextType(this.table.getArrayType(elementType));

        return null;
    }

    @Override
    public Object visit(Length length) {
        length.getArray().accept(this);
        return null;
    }

    @Override
    public Object visit(MathBinaryOp binaryOp) {
        binaryOp.getFirstOperand().accept(this);
        if (this.error != null) return null;
        binaryOp.getSecondOperand().accept(this);
        return null;
    }

    @Override
    public Object visit(LogicalBinaryOp binaryOp) {
        binaryOp.getFirstOperand().accept(this);
        if (this.error != null) return null;
        binaryOp.getSecondOperand().accept(this);
        return null;
    }

    @Override
    public Object visit(MathUnaryOp unaryOp) {
        unaryOp.getOperand().accept(this);
        return null;
    }

    @Override
    public Object visit(LogicalUnaryOp unaryOp) {
        unaryOp.getOperand().accept(this);
        return null;
    }

    @Override
    public Object visit(Literal literal) {
        return null;
    }

    @Override
    public Object visit(ExpressionBlock expressionBlock) {
        return expressionBlock.getExpression().accept(this);
    }
}
